package com.chenshu.doc_searchengine.utils;

import com.chenshu.doc_searchengine.common.Config;
import com.chenshu.doc_searchengine.model.DocInfo;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.ansj.domain.Term;
import org.ansj.splitWord.analysis.ToAnalysis;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Created with IntelliJ IDEA
 * Description：实现了索引的数据结构，并且提供一些api
 * User：chenshu
 * Date：2024-05-03
 * Time：12:57
 */
@Slf4j
public class Index {
    private static final String INDEX_PATH;

    static {
        if (Config.isOnline) {
            INDEX_PATH = "/root/project/document-search-engine/doc_search_index";
        } else {
            INDEX_PATH = "/Users/chenshu/Code/project/document-search-engine/doc_search_index";
        }
    }

    private final ObjectMapper objectMapper = new ObjectMapper();

    // 用数组来表示正排索引
    private ArrayList<DocInfo> forwardIndex = new ArrayList<>();

    // 使用哈希表来表示倒排索引
    private HashMap<String, ArrayList<Weight>> invertedIndex = new HashMap<>();

    //创建两个锁对象，分别给正排索引和倒排索引的操作进行加锁
    private final Object forwardLocker = new Object();
    private final Object invertedLocker = new Object();

    /**
     * 给定docId，在正排索引中，查询文档的详细信息 O(1)
     * @param docId
     * @return
     */
    public DocInfo getDocInfo(int docId) {
        return forwardIndex.get(docId);
    }

    /**
     * 给定一个词，在倒排索引中，查哪些文档和这个词关联 O(1)
     * @param term 词
     * @return List(权重+文档信息)
     */
    public List<Weight> getInverted(String term) {
        return invertedIndex.get(term);
    }

    /**
     * 往索引中新增一个文档
     * @param title 标题
     * @param url 链接
     * @param content 内容
     */
    public void addDoc(String title, String url, String content) {
        //构建正排索引
        DocInfo docInfo = buildForward(title, url, content);
        //构建倒排索引
        buildInverted(docInfo);
    }

    private void buildInverted(DocInfo docInfo) {

        /**
         * 将标题出现次数 和 正文出现次数打包在一起，只需要查一次hashmap
         */
        class WordCnt {
            public int titleCount;
            public int contentCount;
        }

        // 用来统计词频的数据结构
        HashMap<String, WordCnt> wordCntHashMap = new HashMap<>();

        //1. 针对标题进行分词
        List<Term> terms = ToAnalysis.parse(docInfo.getTitle()).getTerms();
        //2. 遍历分词结果，统计每个词出现的次数
        for (Term term : terms) {
            //判定term是否存在
            String word = term.getName();
            WordCnt wordCnt = wordCntHashMap.get(word);
            if (wordCnt == null) {
                //不存在，创建一个新的键值对，titleCount 设为 1
                WordCnt newWordCnt = new WordCnt();
                newWordCnt.titleCount = 1;
                wordCntHashMap.put(word, newWordCnt);
            } else {
                //存在，就找到之前的值，然后把对应的titleCount + 1
                wordCnt.titleCount += 1;
            }
        }

        //3. 针对正文进行分词
        terms = ToAnalysis.parse(docInfo.getContent()).getTerms();
        //4. 遍历分词结果，统计每个词出现的次数
        for (Term term : terms) {
            String word = term.getName();
            WordCnt wordCnt = wordCntHashMap.get(word);
            if (wordCnt == null) {
                //不存在，创建一个新的键值对，contentCount 设为 1
                WordCnt newWordCnt = new WordCnt();
                newWordCnt.contentCount = 1;
                wordCntHashMap.put(word, newWordCnt);
            } else {
                //存在，就找到之前的值，然后把对应的contentCount + 1
                wordCnt.contentCount += 1;
            }
        }


        //5. 遍历刚才这个HashMap，依次更新倒排索引中的结构
        for (Map.Entry<String, WordCnt> entry : wordCntHashMap.entrySet()) {
            synchronized (invertedLocker) {
                // 先根据这里的词去倒排索引中查找
                // 倒排拉链
                List<Weight> invertedList = invertedIndex.get(entry.getKey());
                if (invertedList == null) {
                    // 为空，插入新的键值对
                    ArrayList<Weight> newInvertedList = new ArrayList<>();
                    // 把新的文档，构造为Weight对象，插入进来
                    Weight weight = new Weight();
                    weight.setDocId(docInfo.getDocId());
                    // 权重公式：标题次数*10 + 正文次数
                    weight.setWeight(entry.getValue().titleCount * 10 + entry.getValue().contentCount);
                    newInvertedList.add(weight);
                    invertedIndex.put(entry.getKey(), newInvertedList);
                } else {
                    // 非空，构造出Weight对象，插入到倒排拉链的后面
                    Weight weight = new Weight();
                    weight.setDocId(docInfo.getDocId());
                    weight.setWeight(entry.getValue().titleCount * 10 + entry.getValue().contentCount);
                    invertedList.add(weight);
                }
            }
        }
    }

    private DocInfo buildForward(String title, String url, String content) {
        DocInfo docInfo = new DocInfo();
        docInfo.setTitle(title);
        docInfo.setUrl(url);
        docInfo.setContent(content);
        synchronized (forwardLocker) {
            docInfo.setDocId(forwardIndex.size());
            forwardIndex.add(docInfo);
        }
        return docInfo;
    }

    /**
     * 使用两个文件保存正排索引和倒排索引
     */
    public void save() {
        long begin = System.currentTimeMillis();
        log.info("保存索引开始！");
        File indexPathFile = new File(INDEX_PATH);
        if (!indexPathFile.exists()) {
            indexPathFile.mkdirs();
        }
        File forwardIndexFile = new File(INDEX_PATH + "/forward.txt");
        File invertedIndexFile = new File(INDEX_PATH + "/inverted.txt");
        try {
            objectMapper.writeValue(forwardIndexFile, forwardIndex);
            objectMapper.writeValue(invertedIndexFile, invertedIndex);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        long end = System.currentTimeMillis();
        log.info("保存索引完成！花费时间：" + (end - begin) + "ms");

    }

    /**
     * 把磁盘中的索引数据加载到内存中
     */
    public void load() {
        long begin = System.currentTimeMillis();
        log.info("加载索引开始！");
        File forwardIndexFile = new File(INDEX_PATH + "/forward.txt");
        File invertedIndexFile = new File(INDEX_PATH + "/inverted.txt");
        try {
            forwardIndex = objectMapper.readValue(forwardIndexFile, new TypeReference<ArrayList<DocInfo>>() {});
            invertedIndex = objectMapper.readValue(invertedIndexFile, new TypeReference<HashMap<String, ArrayList<Weight>>>() {});
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        long end = System.currentTimeMillis();
        log.info("加载索引结束！" + (end - begin) + "ms");
    }

    public static void main(String[] args) {
        Index index = new Index();
        index.load();
        System.out.println("索引加载完成");
    }
}
